fix: accept string node ids from the Files sidebar when requesting a signature - #8367
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 221 files with indirect coverage changes 🚀 New features to boost your workflow:
|
vitormattos
left a comment
There was a problem hiding this comment.
I think we need to follow the ID value through the complete flow before normalizing nodeId, fileId, and id with the same helper.
LibreSign already completed the migration to the current meaning where:
nodeId/signedNodeIdidentify Nextcloud nodes;id/fileId/parentFileIdidentify records inlibresign_file.
If any current code still uses fileId for a Nextcloud node ID, that should be treated as a leftover from the old model and corrected, not as a valid alternative meaning.
We already found at least one example of this kind of leftover: RequestSignatureService still has a local $fileId whose value comes from Node::getId() or file.nodeId, and it is then passed to getByNodeId(). In other places, fileId correctly goes to getById().
Because of this history, I do not think we should infer what an ID means only from its property name. We need to follow the actual value.
Looking more about this, I found an important Nextcloud change to consider. In the current @nextcloud/files API, Node.id is string | undefined. Node.fileid is the legacy numeric property and is deprecated. The string representation is intentional because Nextcloud is moving to 64-bit snowflake IDs, which cannot always be represented safely as JavaScript numbers.
Nextcloud documents Snowflake IDs here:
https://docs.nextcloud.com/server/stable/developer_manual/digging_deeper/snowflake_ids.html
They were added in Nextcloud 33 and are 64-bit identifiers. The Nextcloud 33 developer release notes also mention that APIs migrated to Snowflake IDs use strings instead of integers:
https://docs.nextcloud.com/server/stable/developer_manual/release_notes/previous/upgrade_to_33.html
Could we first trace the exact #8363 flow and document where the value comes from and what it represents at each step?
For example:
Nextcloud Node -> tab.ts -> AppFilesTab -> files store -> serializeRequestFile() -> request-signature API -> backend lookup
For each value used as nodeId, fileId, or id in this flow, please verify:
- where the value originates;
- whether it identifies a Nextcloud node or a
libresign_filerow; - whether it is renamed or transformed on the way;
- which backend lookup finally consumes it (
getByNodeId(),getById(), NextcloudgetById(), etc.).
If this flow still uses fileId for a Nextcloud node ID anywhere, that should be corrected as part of the leftover cleanup from the completed migration.
The fix should happen at the point where the representation first becomes incorrect.
In particular, converting a Nextcloud Node.id string with Number() is not safe for future snowflake IDs. A value above Number.MAX_SAFE_INTEGER can silently become a different ID.
I would therefore avoid making serializeRequestFile() generally accept and convert numeric strings until we know which representations are valid for each field.
Please also avoid using an artificial state such as nodeId: 'temp-node' to define the domain model unless production code can really produce that value. The regression tests should reproduce the real sidebar data path as closely as possible.
While following this flow, please also check the test coverage of every method or branch that needs to be changed. If the relevant behavior is not already covered, please add a focused test before or together with the change. The tests should protect the real ID semantics and the complete regression path, not only the final serializer output.
This PR does not need to audit every ID in LibreSign. It should trace and fix the complete #8363 path. If that investigation exposes other leftovers from the old fileId = Nextcloud node ID model, we can handle those in a separate cleanup issue.
|
Thanks — agreed on all three points, and the trace changed my view of where the fix belongs. Below is the #8363 path on Where the string comes from
Then, in The path
Where the fix belongsTwo places make the representation incorrect, and neither should convert with
One decision I need from you — the API contract.
I lean to (a); it is the honest description of what the endpoint accepts, and it is one line plus generated files. Tests (real sidebar data, no
|
|
Thanks, this trace is much clearer, and the proposed direction looks consistent with what I found as well. I checked the flow against the current LibreSign code and the current Nextcloud contracts. The important distinction seems to be:
So the flow that makes the most sense for LibreSign is:
This also means converting the value with I would also treat the Your trace of the LibreSign path also looks correct to me:
I think the typing is especially important here. On the frontend, the type should reflect the real On the backend, I would prefer to validate and normalize the HTTP value once at a clear boundary, then let strong The intended model would be:
If the current data structure makes it difficult to propagate the normalized value, a small shared normalizer would be preferable to repeated casts in different methods. Because of that, I think the safer frontend change would be to keep the fix specific to For the API input, I think we can make the decision here: Could you also check what OpenAPI is actually generated from the proposed Internally, PHP and database values can remain I would keep changing The test plan looks good. Using a real node ID above I also agree with leaving the other leftovers out of this PR. Since some old For backports, I would check affectedness rather than only whether the patch cherry-picks cleanly.
With that, the direction of the rewrite looks good to me. |
9a5292e to
86e816a
Compare
|
Rewritten along the lines you described — the description is updated with the new shape, the generated schema and the manual verification. Short version of the points you asked me to check:
One open point, in the description: |
vitormattos
left a comment
There was a problem hiding this comment.
The new direction looks good to me. I think there are only two points left to clarify before this is ready:
-
LibresignNewFileis shared by other endpoints. IfnodeIdnow acceptsinteger | string, could you check the other consumers and make sure they also support that input? Otherwise, it may be better to keep this wider input type specific to the request-signature API. -
Since
normalizeNodeId()is the boundary where the HTTP value becomes a PHPint, I think it would be safer if it either returns a validintor rejects the value. Leaving an invalid numeric string untouched can still allow later code to cast it differently.
Other than that, the rewrite looks aligned with the #8363 flow and the test coverage is much better now.
86e816a to
ed4a0d9
Compare
|
Both points addressed (folded into the backend commit, rebased on
The description is updated accordingly. |
vitormattos
left a comment
There was a problem hiding this comment.
The two previous concerns are addressed now: the shared LibresignNewFile contract is handled at all its current boundaries, and invalid string values are rejected before they can reach later casts.
The #8363 flow and the overall implementation look good to me now.
I left one small inline comment about keeping normalizeNodeId() fully consistent with the declared non-negative-int contract.
One other point is about the backports. stable34 is confirmed affected. For stable33, I think we should still verify affectedness before opening the backport.
Since the backend architecture in those branches is different from main, I would also avoid treating the backports as cherry-picks of this implementation. They will probably need branch-specific fixes at the equivalent validation/API boundaries. The goal should be to reproduce the same behaviour with the smallest change appropriate for each stable, without bringing the newer main architecture into them.
So I would describe it for now as:
stable34: confirmed affected; prepare a branch-specific backport;stable33: verify affectedness first, then prepare a branch-specific backport if confirmed;stable32: not affected by this Nextcloud ID change.
After the inline point is addressed and CI is green, I think this should be ready.
ed4a0d9 to
69ab59c
Compare
`LibresignNewFile.nodeId` was declared as an integer only, but the Files app exposes node ids as strings (`Node.id` of `@nextcloud/files`) and, since Nextcloud 33, they can exceed what a JavaScript number holds, so the client cannot safely convert them. A string reached the services raw: `RequestSignatureService::saveFile()` swallowed the TypeError of `FileMapper::getByNodeId(int)` and `FileService::getNodeFromData()` propagated the one of `FolderService::getFileByNodeId(int)` as a 422. Validate and normalize the value once, at the boundary between the HTTP payload and the services, and keep `int` from there on: - `FileInputValidator::normalizeNodeId()` turns a string of digits into an int (PHP integers are 64-bit, so node ids are safe there) and leaves anything else untouched for `validateFile()` to reject as before. - `RequestSignatureWorkflowService::createRequest()` and `updateExistingRequest()` apply it to `file` and to each entry of `files` before building the payload the services receive, so the `nodeId?: int` contract of `saveFile()` holds without further casts. - The local `$fileId` of `saveFile()` is renamed to `$nodeId`: it always held a Nextcloud node id. - `LibresignNewFile.nodeId` is declared as `non-negative-int|numeric-string` (generated schema: `oneOf` integer/string; TypeScript: `number | string`) and the parameter description states the accepted forms. Responses are unchanged. Tests cover the string and the integer forms on each changed method, including a node id above `Number.MAX_SAFE_INTEGER`, and the `nodeId` branch of `saveFile()`, which had no coverage. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
…gnature `tab.ts` builds `fileInfo.id` from `node.fileid ?? node.id`. With `@nextcloud/files`, `fileid` is the deprecated numeric getter and is `undefined` when the id does not fit a JavaScript number, while `id` is always a string; a file copied in the Files app and opened in the sidebar hits this path too. `AppFilesTab.update()` stored that string as `nodeId` unchanged, but `serializeRequestFile()` only accepted numbers, so the request went out without `file` and the API answered 422 "File or files parameter is required". Keep `nodeId` in the string domain instead of converting it: the serializer forwards a positive integer or a string of digits as it is, and the API accepts both. `fileId` and `id` only ever come from the LibreSign API as numbers and are not touched. The `FileInfo.id` type of `AppFilesTab` now matches what `tab.ts` sends. Tests follow the sidebar path with a node id above `Number.MAX_SAFE_INTEGER`: `tab.ts` keeps the string, `AppFilesTab` passes it through, and the store sends it unchanged for a single file and for envelope files; the empty id `tab.ts` falls back to is still not sent. Resolves: LibreSign#8363 Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
69ab59c to
b8abb45
Compare
|
Done (folded into the backend commit):
The earlier CI failure was |
vitormattos
left a comment
There was a problem hiding this comment.
Thanks. The remaining points are addressed now and the implementation looks consistent with the agreed nodeId contract.
The #8363 flow, API boundaries, validation and tests look good to me.
|
/backport to stable35 |
|
/backport to stable34 |
|
/backport to stable33 |
|
Backports: |
Backport of the backend part of LibreSign#8367 for stable34, at this branch's own boundary: the Files app sends node ids as strings (`Node.id` of `@nextcloud/files`, 64-bit since Nextcloud 33), and a string reached the services raw — `RequestSignatureService::saveFile()` swallowed the TypeError of `getByNodeId(int)` and `FileService::getNodeFromData()` propagated the one of `getFileByNodeId(int)` as a 422. `RequestSignatureController::normalizeNodeId()` turns a non-negative int or its canonical decimal string into an int once, for `file` and each entry of `files`, on both the create and the update entry points, and rejects anything else with the existing "Invalid fileID" error before it can reach a later cast. Same shape as the stable35 backport. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
Backport of the backend part of #8367 for stable33, at this branch's own boundary: the Files app sends node ids as strings (`Node.id` of `@nextcloud/files`, 64-bit since Nextcloud 33), and a string reached the services raw — `RequestSignatureService::saveFile()` swallowed the TypeError of `getByNodeId(int)` and `FileService::getNodeFromData()` propagated the one of `getFileByNodeId(int)` as a 422. `RequestSignatureController::normalizeNodeId()` turns a non-negative int or its canonical decimal string into an int once, for `file` and each entry of `files`, on both the create and the update entry points, and rejects anything else with the existing "Invalid fileID" error before it can reach a later cast. Same shape as the stable35 backport. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
Resolves: #8363
📝 Summary
Rewritten after the trace in the comments (previous approach — a generic
Number()helper fornodeId/fileId/id— dropped).@nextcloud/filesexposesNode.idas a string;Node.fileidis the deprecated numeric getter and isundefinedwhen the id does not fit a JavaScript number (Nextcloud ≥ 33).tab.tssends that string toAppFilesTab, which stores it asnodeId, andserializeRequestFile()only accepted numbers — the request went out withoutfileand the API answered 422 File or files parameter is required. Had the string been forwarded, the backend would have failed too:saveFile()swallowed theTypeErrorofgetByNodeId(int)andgetNodeFromData()propagated the one ofgetFileByNodeId(int).The model is the one agreed above —
Node.id (string) → HTTP payload (int | decimal string) → validate/normalize once at the PHP boundary → int → OCP Files API:Backend (first commit)
FileInputValidator::normalizeNodeId()is the boundary where the HTTP value becomes a PHPint, and it enforces the declarednon-negative-intcontract: a non-negativeintpasses, its canonical decimal string (digits only, no sign, no leading zeros, within the int range) becomes anint, and anything else present is rejected there with the validator's existing Invalid fileID error (negative int, leading zeros such as"0042", sign, decimals, spaces, empty string, overflow, non-numeric, non-scalar). Nothing invalid reaches a later cast.LibresignNewFile, so the shared schema holds everywhere it is declared:RequestSignatureWorkflowService::createRequest()/updateExistingRequest()(fileand each entry offiles),FileController::prepareFilesForSaving()(POST /file,FileInputValidatorinjected instead of extending theValidateHelperfaçade that Remove the ValidateHelper compatibility facade #8357 removes) andIdDocsService::addIdDocs()/addFilesToDocumentFolder()(POST /id-docs, the error keeps the per-file JSON shape with the file index). Past these points thenodeId?: intcontract ofsaveFile()holds without casts in the services.saveFile(): the local$fileIdthat held a node id is renamed$nodeId.LibresignNewFile.nodeIdis declarednon-negative-int|numeric-string; the parameter description says "a non-negative integer or its canonical decimal string". Responses are unchanged.Frontend (second commit) — specific to
nodeId, noNumber()serializeRequestFile()forwardsnodeIdwhen it is a positive integer or a canonical decimal string (/^[1-9][0-9]*$/), as it is.fileId/idkeep the number-only checks.AppFilesTab'sFileInfo.idbecomesnumber | string, matching whattab.tssends.About the generated OpenAPI
openapi-extractormapsnumeric-stringto a plainstringand has nopatternsupport, so the generated schema isand the TypeScript type is
nodeId?: number | string. The "canonical decimal string" part lives in the parameter description and in the validator (422 Invalid fileID for anything else). If you would rather have a stricter schema, I can hand-write it, but it would be lost at the nextcomposer openapi.🧪 How to test
Unit tests use
9007199254740993(Number.MAX_SAFE_INTEGER + 2) and keep the integer path on each changed method:FileInputValidatorTest— accepted forms (int, zero, canonical decimal string, aboveNumber.MAX_SAFE_INTEGER, 64-bit, null/absent) and 11 rejected forms (negative int included);RequestSignatureWorkflowServiceTest(realFileInputValidator) — string normalized once forfile, integer kept, each entry offiles,updateExistingRequest, invalid value rejected before any service runs;IdDocsServiceTest— string normalized beforesaveFile(), invalid value reported with the file index;RequestSignatureServiceTest::testSaveFileReusesTheFileRegisteredForTheNodeId(thenodeIdbranch ofsaveFile()had no coverage).tab.spec.tskeeps the string id of a node withoutfileid;AppFilesTab.spec.tspasses it through;files.spec.tssends it unchanged for a single file and for envelope files (two of these fail onmain), and the empty idtab.tsfalls back to is still not sent.Local checks: Vitest 127/127 on the three specs, ESLint and
vue-tscclean; php-cs-fixer clean; psalm on the changed files: only the pre-existingMissingDependencyerrors; PHPUnit for the five touched PHP classes: OK. API check on the devcontainer:request-signatureandPOST /filewith"nodeId": "temp-node"/"0042"/"abc"→ 422 Invalid fileID; with a string of digits → 200 and the node resolved.Manual verification (devcontainer, Nextcloud 36 dev)
A real node id above
Number.MAX_SAFE_INTEGER: PDF uploaded via WebDAV, itsfileidset to9007199254740993inoc_filecache(PROPFIND returns it), then Files → Details → LibreSign tab → Add signer.window.OCA.Libresign.fileInfo.idis the string"9007199254740993"(fileidisundefinedon that node).Before (
mainbundle): saving the signer sendsPATCH /request-signaturewith"file": null→ 422 — the message from the issue:After: the same step sends
"file": {"nodeId": "9007199254740993"}→ 200; thelibresign_filerow hasnode_id = 9007199254740993(BIGINT), and Request signatures → Send completes:Two things seen on the way, not touched here: the Files app itself logs Failed to open sidebar on file 9007199254740992 (core,
fileidcoerced to a number), and the placeholder-fileInfo.idproduces aGET /file/validate/file_id/-9007199254740992404 (the unary-minus leftover already listed).⚙️ API / Back‑end changes
LibresignNewFile.nodeIdacceptsinteger | string of digits(request input only); generatedopenapi*.jsonandsrc/types/openapi/*.tsupdatedFileInputValidator::normalizeNodeId(array $file, int $type): array(returns the payload with anintnode id or throwsLibresignException);FileInputValidatorinjected intoRequestSignatureWorkflowServiceandFileController🚧 Backport
Not cherry-picks of this implementation — the backend architecture of the stable branches differs from
main, so each backport reproduces the same behaviour with the smallest change at that branch's own validation/API boundary, without bringing themainarchitecture in:stable34: confirmed affected; branch-specific backport after merge;stable33: verify affectedness first (string node ids start with Nextcloud 33), then a branch-specific backport if confirmed;stable32: not affected by this Nextcloud ID change.Leftovers for a separate issue (unchanged, as agreed)
openInLibreSignAction.js:77(fileIdcarrying a node id),AppFilesTab.vueparseIntonhandleNodeDeleted,showStatusInlineAction.js:12,SignFileService.php:143,getFileIdByNodeId()strict===and the-fileInfo.idplaceholder key,getSelectedFileView()dropping a stringnodeId.✅ Checklist
🤖 AI (if applicable)